/*
 * Copyright (c) 2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import Unsafe from './Unsafe';
import FseCompressionTable from './FseCompressionTable';
import { FiniteStateEntropy, Table } from './FiniteStateEntropy'
import Util from './Util';
import Long from '../util/long/index'

export default class FseTableReader {
    private nextSymbol: Int16Array = new Int16Array(FiniteStateEntropy.MAX_SYMBOL + 1);
    private normalizedCounters: Int16Array = new Int16Array(FiniteStateEntropy.MAX_SYMBOL + 1);

    public readFseTable(table: Table, inputBase: any, inputAddress: Long, inputLimit: Long, maxSymbol: number, maxTableLog: number): number{
        let input: Long = inputAddress;
        Util.verify(inputLimit.sub(inputAddress).greaterThanOrEqual(4), input, "Not enough input bytes");

        let threshold: number;
        let symbolNumber: number = 0;
        let previousIsZero: boolean = false;

        let bitStream: number = Unsafe.getInt(inputBase, input.toNumber());

        let tableLog: number = (bitStream & 0xF) + FiniteStateEntropy.MIN_TABLE_LOG;

        let numberOfBits: number = tableLog + 1;
        bitStream >>>= 4;
        let bitCount: number = 4;

        Util.verify(tableLog <= maxTableLog, input, "FSE table size exceeds maximum allowed size");

        let remaining: number = (1 << tableLog) + 1;
        threshold = 1 << tableLog;

        while (remaining > 1 && symbolNumber <= maxSymbol) {
            if (previousIsZero) {
                let n0: number = symbolNumber;
                while ((bitStream & 0xFFFF) == 0xFFFF) {
                    n0 += 24;
                    if (input.lessThan(inputLimit.sub(5))) {
                        input = input.add(2);
                        bitStream = (Unsafe.getInt(inputBase, input.toNumber()) >>> bitCount);
                    } else {
                        bitStream >>>= 16;
                        bitCount += 16;
                    }
                }
                while ((bitStream & 3) == 3) {
                    n0 += 3;
                    bitStream >>>= 2;
                    bitCount += 2;
                }
                n0 += bitStream & 3;
                bitCount += 2;

                Util.verify(n0 <= maxSymbol, input, "Symbol larger than max value");

                while (symbolNumber < n0) {
                    this.normalizedCounters[symbolNumber++] = 0;
                }
                if ((input.lessThanOrEqual(inputLimit.sub(7))) || (
                input.add(bitCount >>> 3)
                    .lessThanOrEqual(inputLimit.sub(4)))) {
                    input = input.add(bitCount >>> 3);
                    bitCount &= 7;
                    bitStream = Unsafe.getInt(inputBase, input.toNumber()) >>> bitCount;
                } else {
                    bitStream >>>= 2;
                }
            }

            let max: number = (2 * threshold - 1) - remaining;
            let count: number;

            if ((bitStream & (threshold - 1)) < max) {
                count = bitStream & (threshold - 1);
                bitCount += numberOfBits - 1;
            }
            else {
                count = bitStream & (2 * threshold - 1);
                if (count >= threshold) {
                    count -= max;
                }
                bitCount += numberOfBits;
            }
            count--; // extra accuracy

            remaining -= Math.abs(count);
            this.normalizedCounters[symbolNumber++] = count;
            previousIsZero = count == 0;
            while (remaining < threshold) {
                numberOfBits--;
                threshold >>>= 1;
            }

            if ((input.lessThanOrEqual(inputLimit.sub(7))) || (input.add(bitCount >> 3).lessThanOrEqual(inputLimit.sub(4)))) {
                input = input.add(bitCount >>> 3);
                bitCount &= 7;
            } else {
                bitCount -= Long.fromNumber(8).multiply(inputLimit.sub(4).sub(input)).toInt();
                input = inputLimit.sub(4);
            }
            bitStream = Unsafe.getInt(inputBase, input.toNumber()) >>> (bitCount & 31);
        }

        Util.verify(remaining == 1 && bitCount <= 32, input, "Input is corrupted");

        maxSymbol = symbolNumber - 1;
        Util.verify(maxSymbol <= FiniteStateEntropy.MAX_SYMBOL, input, "Max symbol value too large (too many symbols for FSE)");

        input = input.add((bitCount + 7) >> 3);

        // populate decoding table
        let symbolCount: number = maxSymbol + 1;
        let tableSize: number = 1 << tableLog;
        let highThreshold: number = tableSize - 1;

        table.log2Size = tableLog;

        for (let symbolnum: number = 0; symbolnum < symbolCount; symbolnum++) {
            if (this.normalizedCounters[symbolnum] == -1) {
                table.symbols[highThreshold--] = symbolnum;
                this.nextSymbol[symbolnum] = 1;
            }
            else {
                this.nextSymbol[symbolnum] = this.normalizedCounters[symbolnum];
            }
        }

        let position: number = FseCompressionTable.spreadSymbols(this.normalizedCounters, maxSymbol, tableSize, highThreshold, table.symbols);

        Util.verify(position == 0, input, "Input is corrupted");

        for (let i: number = 0; i < tableSize; i++) {
            let symbolByte: number = table.symbols[i];
            let nextState: number = this.nextSymbol[symbolByte]++;
            table.numberOfBits[i] = tableLog - Util.highestBit(nextState);
            table.newState[i] = (nextState << table.numberOfBits[i]) - tableSize;
        }

        return input.sub(inputAddress).toInt();
    }

    public static initializeRleTable(table: Table, value: number): void
    {
        table.log2Size = 0;
        table.symbols[0] = value;
        table.newState[0] = 0;
        table.numberOfBits[0] = 0;
    }
}
